#include <algorithm>
#include <chrono>
#include <cctype>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <map>
#include <sstream>
#include <stdexcept>
#include <string>
#include <system_error>
#include <vector>

#if defined(__linux__) || defined(__APPLE__)

#include <cstdlib>
#include <unistd.h>

#include "asycipc/linux_diagnostics.hpp"
#include "json11.hpp"

namespace {

namespace fs = std::filesystem;
using json11::Json;

constexpr std::chrono::seconds kDefaultStaleAge(300);

enum class OutputFormat { kTable, kJson };

struct CleanupOptions {
  std::chrono::seconds older_than{600};
  bool force = false;
  bool dry_run = false;
  bool verbose = false;
};

fs::path DetermineRuntimeRoot() {
  if (const char* override_dir = std::getenv("ASYIPC_RUNTIME_DIR")) {
    if (*override_dir != '\0') {
      return fs::path(override_dir);
    }
  }

  if (::geteuid() == 0) {
#if defined(__APPLE__)
    return fs::path("/var/run/asycipc");
#else
    return fs::path("/run/asycipc");
#endif
  }

  if (const char* xdg_dir = std::getenv("XDG_RUNTIME_DIR")) {
    if (*xdg_dir != '\0') {
      return fs::path(xdg_dir) / "asycipc";
    }
  }

  std::error_code ec;
#if defined(__linux__)
  fs::path user_run = fs::path("/run/user") / std::to_string(::geteuid()) / "asycipc";
  if (fs::exists(user_run.parent_path(), ec)) {
    return user_run;
  }
#endif

#if defined(__APPLE__)
  if (const char* tmp = std::getenv("TMPDIR")) {
    if (*tmp) {
      return fs::path(tmp) / "asycipc";
    }
  }
#endif

  return fs::temp_directory_path() / "asycipc";
}

fs::path ResolveBasePath(const std::string& endpoint) {
  if (endpoint.empty()) {
    throw std::runtime_error("endpoint must not be empty");
  }
  if (endpoint.front() == '/') {
    return fs::path(endpoint);
  }
  return DetermineRuntimeRoot() / endpoint;
}

std::string ToLower(std::string value) {
  std::transform(value.begin(), value.end(), value.begin(),
                 [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
  return value;
}

std::string FormatAge(std::chrono::seconds age) {
  if (age == std::chrono::seconds::max()) {
    return "-";
  }
  if (age.count() < 60) {
    return std::to_string(age.count()) + "s";
  }
  const auto minutes = age.count() / 60;
  const auto seconds = age.count() % 60;
  if (minutes < 60) {
    std::ostringstream oss;
    oss << minutes << "m";
    if (seconds != 0) {
      oss << seconds << "s";
    }
    return oss.str();
  }
  const auto hours = minutes / 60;
  const auto rem_minutes = minutes % 60;
  std::ostringstream oss;
  oss << hours << "h";
  if (rem_minutes != 0) {
    oss << rem_minutes << "m";
  }
  return oss.str();
}

std::string DetermineStatus(const asycipc::linux_diagnostics::FifoSessionDetail& info) {
  if (info.is_incomplete) {
    return "incomplete";
  }
  if (info.is_stale) {
    return "stale";
  }
  return "active";
}

void PrintSessionsTable(const std::vector<asycipc::linux_diagnostics::FifoSessionDetail>& sessions,
                        const asycipc::linux_diagnostics::FifoHealthStats& stats) {
  std::cout << std::left << std::setw(26) << "SESSION" << std::setw(6) << "IN"
            << std::setw(8) << "OUT" << std::setw(12) << "AGE" << std::setw(12)
            << "STATUS" << std::endl;
  std::cout << std::string(64, '-') << std::endl;

  for (const auto& info : sessions) {
    std::cout << std::left << std::setw(26) << info.session_id << std::setw(6)
              << (info.has_in ? "yes" : "no") << std::setw(8)
              << (info.has_out ? "yes" : "no") << std::setw(12)
              << FormatAge(info.age) << std::setw(12) << DetermineStatus(info)
              << std::endl;
  }

  std::cout << std::string(64, '-') << std::endl;
  std::cout << "summary: total=" << stats.total_sessions
            << " active=" << stats.active_sessions
            << " incomplete=" << stats.incomplete_sessions
            << " stale=" << stats.stale_sessions << std::endl;
}

void PrintSessionsJson(const std::vector<asycipc::linux_diagnostics::FifoSessionDetail>& sessions,
                       const asycipc::linux_diagnostics::FifoHealthStats& stats) {
  std::vector<Json> session_nodes;
  session_nodes.reserve(sessions.size());
  for (const auto& info : sessions) {
    std::map<std::string, Json> node_obj{
        {"session", Json(info.session_id)},
        {"has_in", Json(info.has_in)},
        {"has_out", Json(info.has_out)},
        {"status", Json(DetermineStatus(info))},
        {"age_seconds",
         info.age == std::chrono::seconds::max()
             ? Json()
             : Json(static_cast<double>(info.age.count()))}};
    session_nodes.emplace_back(node_obj);
  }

  std::map<std::string, Json> summary_obj{
      {"total", Json(static_cast<double>(stats.total_sessions))},
      {"active", Json(static_cast<double>(stats.active_sessions))},
      {"incomplete", Json(static_cast<double>(stats.incomplete_sessions))},
      {"stale", Json(static_cast<double>(stats.stale_sessions))}};
  std::map<std::string, Json> root_obj{
      {"sessions", Json(session_nodes)},
      {"summary", Json(summary_obj)}};
  Json result(root_obj);

  std::cout << result.dump(2) << std::endl;
}

void RunSessionsList(const fs::path& base_dir, OutputFormat format,
                     std::chrono::seconds stale_age) {
  auto sessions = asycipc::linux_diagnostics::CollectFifoSessions(base_dir, stale_age);
  auto stats = asycipc::linux_diagnostics::InspectFifoHealth(base_dir, stale_age);

  if (sessions.empty()) {
    if (format == OutputFormat::kJson) {
      std::map<std::string, Json> empty_summary{
          {"total", Json(0.0)},
          {"active", Json(0.0)},
          {"incomplete", Json(0.0)},
          {"stale", Json(0.0)}};
      std::vector<Json> empty_sessions;
      std::map<std::string, Json> root_obj{
          {"sessions", Json(empty_sessions)},
          {"summary", Json(empty_summary)}};
      Json result(root_obj);
      std::cout << result.dump(2) << std::endl;
    } else {
      std::cout << "no active sessions found under " << base_dir << "\n";
    }
    return;
  }

  if (format == OutputFormat::kJson) {
    PrintSessionsJson(sessions, stats);
  } else {
    PrintSessionsTable(sessions, stats);
  }
}

bool ShouldRemoveSession(const asycipc::linux_diagnostics::FifoSessionDetail& info,
                         const CleanupOptions& options) {
  if (options.force) {
    return true;
  }
  if (info.age == std::chrono::seconds::max()) {
    return true;
  }
  return info.age >= options.older_than;
}

void RunTransportCleanup(const fs::path& base_dir, const CleanupOptions& options) {
  auto sessions = asycipc::linux_diagnostics::CollectFifoSessions(base_dir, options.older_than);
  if (sessions.empty()) {
    std::cout << "no sessions to cleanup under " << base_dir << "\n";
    return;
  }

  int removed = 0;
  std::vector<std::string> skipped;
  for (const auto& info : sessions) {
    if (!ShouldRemoveSession(info, options)) {
      skipped.push_back(info.session_id);
      continue;
    }

    if (options.dry_run) {
      std::cout << "would remove session " << info.session_id << std::endl;
      continue;
    }

    const fs::path in_path = base_dir / ("session-" + info.session_id + "-in");
    const fs::path out_path = base_dir / ("session-" + info.session_id + "-out");

    std::error_code ec_in;
    std::error_code ec_out;
    fs::remove(in_path, ec_in);
    fs::remove(out_path, ec_out);

    const bool in_error = ec_in && ec_in != std::errc::no_such_file_or_directory;
    const bool out_error = ec_out && ec_out != std::errc::no_such_file_or_directory;
    if (in_error || out_error) {
      std::cerr << "failed to remove session " << info.session_id;
      if (in_error) {
        std::cerr << " in=" << ec_in.message();
      }
      if (out_error) {
        std::cerr << " out=" << ec_out.message();
      }
      std::cerr << "\n";
      continue;
    }

    if (options.verbose) {
      std::cout << "removed session " << info.session_id << std::endl;
    }
    ++removed;
  }

  if (!options.dry_run) {
    std::cout << "cleanup completed: removed " << removed << " session(s)";
    if (!skipped.empty()) {
      std::cout << ", skipped " << skipped.size() << " session(s)";
    }
    std::cout << std::endl;
  }

  if (!skipped.empty()) {
    std::cout << "skipped: ";
    for (std::size_t idx = 0; idx < skipped.size(); ++idx) {
      if (idx != 0) {
        std::cout << ", ";
      }
      std::cout << skipped[idx];
    }
    std::cout << std::endl;
  }
}

void PrintUsage(const char* program) {
  std::cerr << "Usage:\n"
            << "  " << program << " sessions list <endpoint> [--format table|json]"
            << " [--stale-age <seconds>]\n"
            << "  " << program
            << " transport cleanup <endpoint> [--older-than <seconds>] [--force]"
            << " [--dry-run] [--verbose]\n"
            << "\nLegacy (deprecated):\n  " << program
            << " <endpoint> sessions|cleanup [options]\n";
}

OutputFormat ParseFormat(const std::string& value) {
  const std::string lower = ToLower(value);
  if (lower == "json") {
    return OutputFormat::kJson;
  }
  if (lower == "table") {
    return OutputFormat::kTable;
  }
  throw std::runtime_error("unknown format: " + value);
}

bool HandleSessionsCommand(char** argv, int argc) {
  if (argc < 4) {
    return false;
  }
  const std::string subcommand = argv[2];
  if (subcommand != "list") {
    return false;
  }
  const std::string endpoint = argv[3];

  OutputFormat format = OutputFormat::kTable;
  std::chrono::seconds stale_age = kDefaultStaleAge;
  for (int i = 4; i < argc; ++i) {
    const std::string arg = argv[i];
    if (arg == "--format") {
      if (i + 1 >= argc) {
        throw std::runtime_error("--format requires a value");
      }
      format = ParseFormat(argv[++i]);
    } else if (arg == "--stale-age") {
      if (i + 1 >= argc) {
        throw std::runtime_error("--stale-age requires a value");
      }
      stale_age = std::chrono::seconds(std::stoll(argv[++i]));
    } else {
      throw std::runtime_error("unknown option: " + arg);
    }
  }

  auto base_dir = ResolveBasePath(endpoint);
  RunSessionsList(base_dir, format, stale_age);
  return true;
}

bool HandleTransportCommand(char** argv, int argc) {
  if (argc < 4) {
    return false;
  }
  const std::string subcommand = argv[2];
  if (subcommand != "cleanup") {
    return false;
  }
  const std::string endpoint = argv[3];

  CleanupOptions options;
  for (int i = 4; i < argc; ++i) {
    const std::string arg = argv[i];
    if (arg == "--older-than") {
      if (i + 1 >= argc) {
        throw std::runtime_error("--older-than requires a value");
      }
      options.older_than = std::chrono::seconds(std::stoll(argv[++i]));
    } else if (arg == "--force") {
      options.force = true;
    } else if (arg == "--dry-run") {
      options.dry_run = true;
    } else if (arg == "--verbose") {
      options.verbose = true;
    } else {
      throw std::runtime_error("unknown option: " + arg);
    }
  }

  auto base_dir = ResolveBasePath(endpoint);
  RunTransportCleanup(base_dir, options);
  return true;
}

int HandleLegacyCommand(int argc, char** argv) {
  if (argc < 3) {
    PrintUsage(argv[0]);
    return 1;
  }
  std::cerr << "[asycipcctl] legacy invocation detected; please switch to"
            << " 'sessions list' or 'transport cleanup' syntax" << std::endl;

  const std::string endpoint = argv[1];
  const std::string command = argv[2];
  fs::path base_dir = ResolveBasePath(endpoint);

  try {
    if (command == "sessions") {
      RunSessionsList(base_dir, OutputFormat::kTable, kDefaultStaleAge);
      return 0;
    }
    if (command == "cleanup") {
      CleanupOptions options;
      for (int i = 3; i < argc; ++i) {
        const std::string arg = argv[i];
        if (arg == "--older-than") {
          if (i + 1 >= argc) {
            throw std::runtime_error("--older-than requires a value");
          }
          options.older_than = std::chrono::seconds(std::stoll(argv[++i]));
        } else if (arg == "--force") {
          options.force = true;
        } else {
          throw std::runtime_error("unknown option: " + arg);
        }
      }
      RunTransportCleanup(base_dir, options);
      return 0;
    }
  } catch (const std::exception& ex) {
    std::cerr << ex.what() << std::endl;
    return 1;
  }

  std::cerr << "unknown command: " << command << std::endl;
  PrintUsage(argv[0]);
  return 1;
}

}  // namespace

int main(int argc, char** argv) {
  if (argc < 2) {
    PrintUsage(argv[0]);
    return 1;
  }

  const std::string category = argv[1];

  try {
    if (category == "sessions") {
      if (!HandleSessionsCommand(argv, argc)) {
        PrintUsage(argv[0]);
        return 1;
      }
      return 0;
    }

    if (category == "transport") {
      if (!HandleTransportCommand(argv, argc)) {
        PrintUsage(argv[0]);
        return 1;
      }
      return 0;
    }
  } catch (const std::exception& ex) {
    std::cerr << ex.what() << std::endl;
    return 1;
  }

  return HandleLegacyCommand(argc, argv);
}

#else

int main(int, char**) {
  std::cerr << "asycipcctl is only available on Linux systems" << std::endl;
  return 1;
}

#endif  // __linux__
